/**
 * 
 */
package edu.umd.clip.lm.util;

import java.io.*;
import java.util.*;

/**
 * @author Denis Filimonov <den@cs.umd.edu>
 *
 * Implements OutputStream that can be detached from open file, storing output in a temporary buffer
 * Can be useful for concurrent output to a large number of files (larger than the open file limit)  
 */
public class DetachableOutputStream extends OutputStream {
	private final File backendFile;
	private final int maxBufferSize;
	private final GrowingOutputBuffer buffer;
	private final FileOpenObservable observable;

	private FileOutputStream backendStream;
	private int bufferSize;
	
	/**
	 * @param backendFile
	 * @param maxBufferSize
	 * @param bufferSizeIncrement
	 */
	public DetachableOutputStream(File backendFile, int maxBufferSize, int bufferSizeIncrement) {
		this.backendFile = backendFile;
		this.maxBufferSize = maxBufferSize;
		this.buffer = new GrowingOutputBuffer(bufferSizeIncrement);
		this.bufferSize = 0;
		this.observable = new FileOpenObservable();
	}

	/* (non-Javadoc)
	 * @see java.io.OutputStream#write(int)
	 */
	@Override
	public synchronized void write(int b) throws IOException {
		if (bufferSize >= maxBufferSize) {
			openBackend();
		}
		if (backendStream != null) {
			backendStream.write(b);
		} else {
			buffer.put((byte) b);
			bufferSize += 1;
		}
	}

	@Override
	public synchronized void write(byte[] b, int off, int len) throws IOException {
		if (bufferSize + len > maxBufferSize) {
			openBackend();
		}
		if (backendStream != null) {
			backendStream.write(b, off, len);
		} else {
			buffer.put(b, off, len);
			bufferSize += len;
		}
	}

	@Override
	public synchronized void flush() throws IOException {
		if (bufferSize > 0) {
			openBackend();
		}
		if (backendStream != null) {
			backendStream.flush();
		}
	}

	@Override
	public synchronized void close() throws IOException {
		if (bufferSize > 0) {
			openBackend();
		}
		if (backendStream != null) {
			backendStream.close();
			backendStream = null;
		}
	}

	private void openBackend() throws IOException {
		observable.setChanged();
		observable.notifyObservers(this);
		observable.clearChanged();
		
		backendStream = new FileOutputStream(backendFile, true);
		if (bufferSize > 0) {
			buffer.flip();
			buffer.drainTo(backendStream.getChannel());
			bufferSize = 0;
		}
	}
	
	public synchronized void detach() throws IOException {
		if (backendStream == null) return;
		
		if (bufferSize > 0) {
			buffer.flip();
			buffer.drainTo(backendStream.getChannel());
			bufferSize = 0;			
		}
		
		backendStream.close();
		backendStream = null;
	}

	public File getBackendFile() {
		return backendFile;
	}

	public int getBufferSize() {
		return bufferSize;
	}

	public void addFileOpenListener(Observer o) {
		observable.addObserver(o);
	}
	
	private static class FileOpenObservable extends Observable {
		@Override
		protected synchronized void setChanged() {
			super.setChanged();
		}

		@Override
		protected synchronized void clearChanged() {
			super.clearChanged();
		}
	}
		
}
